﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using ReAnimation.Helpers.Common;
using ReAnimation.Infrastructure.Services.Interfaces;
using ReAnimation.Domain;
using OpenBabel;
using System.Collections;
using System.Globalization;
using ReAnimation.DAL.RepositoryInterfaces;
using System.Text.RegularExpressions;

namespace ReAnimation.Infrastructure.Services
{
    public class OpenBabelService: IOpenBabelService
    {
        private IAtomDescriptionRepository _atomDescriptionRepository;

        public OpenBabelService(IAtomDescriptionRepository atomDescriptionRepository)
        {
            _atomDescriptionRepository = atomDescriptionRepository;
        }

        #region IOpenBabelService Members

        public OBMol OpenOBMolFromFile(string fileName)
        {
            OBConversion conversion = new OBConversion();
            conversion.SetInFormat(fileName.Substring(fileName.LastIndexOf(".")+1));
            OBMol mol = new OBMol();

            if (conversion.ReadFile(mol, fileName))
                return mol;

            return null;
        }

        public ZMatrix ConvertOBMolToZMatrix(OBMol molecule)
        {
            OBConversion conversion = new OBConversion();
            conversion.SetOutFormat("gzmat");

            String zMatrixStr = conversion.WriteString(molecule);
            return ParseZMatrixFromGzmatString(zMatrixStr);
        }

        public OBMol ConvertZMatrixToOBMol(ZMatrix zMatrix)
        {
            OBConversion conversion = new OBConversion();
            conversion.SetInFormat("gzmat");
            OBMol mol = new OBMol();
            conversion.SetOutFormat("xyz");

            string gaussianFormattedZMatrix = ConvertZMatrixIntoGaussianFileFormat(zMatrix);

            if (!conversion.ReadString(mol, gaussianFormattedZMatrix))
                return null;

            return mol;
        }

        public ZMatrix OpenZMatrixFromFile(string fileName)
        {
            OBMol mol = OpenOBMolFromFile(fileName);
            if (mol != null)
                return ConvertOBMolToZMatrix(mol);
            return null;
        }
        
        private string ConvertZMatrixIntoGaussianFileFormat(ZMatrix zMatrix)
        {
            StringBuilder resultStringBuilder = new StringBuilder();

            AppendHeader(zMatrix, resultStringBuilder);
            AppendMainPart(zMatrix, resultStringBuilder);
            AppendTile(zMatrix, resultStringBuilder);

            return resultStringBuilder.ToString();
        }

        private void AppendMainPart(ZMatrix zMatrix, StringBuilder resultStringBuilder)
        {
            for (int i = 3; i < zMatrix.ZMatrixLines.Count; i++)
            {
                int currentLineNumber = i;
                var zMatrixLine = zMatrix.ZMatrixLines[i];

                resultStringBuilder.AppendFormat("{1}  {2}  r{0}  {3}  a{0}  {4}  d{0}",
                                                 currentLineNumber,
                                                 ShortenedAtomName(zMatrixLine.AtomDescription),
                                                 zMatrixLine.AtomConnect,
                                                 zMatrixLine.AngleConnect,
                                                 zMatrixLine.DihedralConnect);
                resultStringBuilder.AppendLine();

            }
        }

        private void AppendHeader(ZMatrix zMatrix, StringBuilder resultStringBuilder)
        {
            resultStringBuilder.AppendLine("#Put Keywords Here, check Charge and Multiplicity.");
            resultStringBuilder.AppendLine();
            resultStringBuilder.AppendLine("/tmp/babel7932.xyz            0.0000000");
            resultStringBuilder.AppendLine();
            resultStringBuilder.AppendLine("0  1");

            resultStringBuilder.AppendLine(ShortenedAtomName(zMatrix.ZMatrixLines[0].AtomDescription));

            resultStringBuilder.AppendFormat("{0}  {1}  r1", ShortenedAtomName(zMatrix.ZMatrixLines[1].AtomDescription),
                                                             zMatrix.ZMatrixLines[1].AtomConnect);

            resultStringBuilder.AppendLine();

            resultStringBuilder.AppendFormat("{0}  {1}  r2  {2}  a2", ShortenedAtomName(zMatrix.ZMatrixLines[2].AtomDescription),
                                                                     zMatrix.ZMatrixLines[2].AtomConnect,
                                                                     zMatrix.ZMatrixLines[2].AngleConnect);

            resultStringBuilder.AppendLine();
        }

        private static void AppendTile(ZMatrix zMatrix, StringBuilder resultStringBuilder)
        {
            resultStringBuilder.AppendLine("Variables:");
            for (int i = 1; i < zMatrix.ZMatrixLines.Count; i++)
                resultStringBuilder.AppendLine("r{0}= {1}".F(i, zMatrix.ZMatrixLines[i].BondDistance));
            for (int i = 2; i < zMatrix.ZMatrixLines.Count; i++)
                resultStringBuilder.AppendLine("a{0}= {1}".F(i, zMatrix.ZMatrixLines[i].BondAngle));
            for (int i = 3; i < zMatrix.ZMatrixLines.Count; i++)
                resultStringBuilder.AppendLine("d{0}= {1}".F(i, zMatrix.ZMatrixLines[i].DihedralAngle));
        }

        //TODO: may be it will be more adequate to have shortened names directly inside the DB ?
        private string ShortenedAtomName(AtomDescription atomDescription)
        {
            return atomDescription.Name.Split(new char[] { ' ' }, StringSplitOptions.RemoveEmptyEntries)[0];
        }

        private ZMatrix ParseZMatrixFromGzmatString(String gzmatString)
        {
            String[] lines = gzmatString.Split(new string[]{"\n"}, 
                                               StringSplitOptions.RemoveEmptyEntries);

            //remove comments
            lines = lines.Select(x =>
                {
                    return x.Split(new[]{"#"},StringSplitOptions.None)[0];
                }).ToArray();
            //remove empty
            lines = lines.Where(x => !String.IsNullOrEmpty(x)).Skip(2).ToArray();
   
            IDictionary<String, float> zMatrixVariables = new Dictionary<String,float>();

            IEnumerable<String> atoms =  lines.TakeWhile(x => x != "Variables:");
            IEnumerable<String> variables = lines.Skip(atoms.Count()+1);
            IDictionary<String, float> variablesValues = new Dictionary<String, float>();

            if (variables.Count() == 0) return null;

            variables.ForEach(x =>
            {
                if (!x.Contains("=")) return;

                String[] components = x.Split(new[]{"="},StringSplitOptions.RemoveEmptyEntries);
                lines = lines.Select(s => s.Trim()).ToArray();

                variablesValues[components[0]] = float.Parse(components[1], CultureInfo.InvariantCulture.NumberFormat);
            });

            if (variablesValues.Keys.Count == 0) return null;

            ZMatrix zmatrix = new ZMatrix();

            atoms.ForEach(x =>
            {
                if(!Regex.IsMatch(x,"^[A-Z]")) return;

                String[] components = x.Split(new[] { " " }, StringSplitOptions.RemoveEmptyEntries);
                components = components.Select(s => s.Trim()).ToArray();

                AtomDescription atomDescription = _atomDescriptionRepository.GetByName(components[0]);

                ZMatrixLine zMatrixLine = new ZMatrixLine();

                zMatrixLine.AtomDescription = atomDescription ?? _atomDescriptionRepository.GetByName("H");

                for (int i = 1; i <= Math.Min(6, components.Count() - 1); i++)
                {
                    switch (i)
                    {
                        case 1:
                            zMatrixLine.AtomConnect = Int32.Parse(components[i]);
                            break;
                        case 2:
                            zMatrixLine.BondDistance = variablesValues[components[i]];
                            break;
                        case 3:
                            zMatrixLine.AngleConnect = Int32.Parse(components[i]);
                            break;
                        case 4:
                            zMatrixLine.BondAngle = variablesValues[components[i]];
                            break;
                        case 5:
                            zMatrixLine.DihedralConnect = Int32.Parse(components[i]);
                            break;
                        case 6:
                            zMatrixLine.DihedralAngle = variablesValues[components[i]];
                            break;
                        default:
                            throw new IndexOutOfRangeException();
                    }
                }
                zmatrix.ZMatrixLines.Add(zMatrixLine);
            });

            return zmatrix;
        }

        #endregion

    }
}
